Skip to content

Java bindings for LP/MIP/QP - #1524

Open
nvidiacbrissette wants to merge 28 commits into
NVIDIA:mainfrom
nvidiacbrissette:cbrissette/cuopt-bindings
Open

Java bindings for LP/MIP/QP#1524
nvidiacbrissette wants to merge 28 commits into
NVIDIA:mainfrom
nvidiacbrissette:cbrissette/cuopt-bindings

Conversation

@nvidiacbrissette

@nvidiacbrissette nvidiacbrissette commented Jul 7, 2026

Copy link
Copy Markdown

Description

Java bindings at parity with the LP/MIP/QP surface, plus tests and docs.

The bindings are hand-written JNI over the public C API. Contributes to #1535 and #860.

Native surface

The bindings originally carried cuopt_java_native_api.{hpp,cpp}, a Java-local shim declaring seven entry points that libcuopt did not export. It reimplemented the memory layout of the opaque cuOptSolverSettings handle in order to reach the underlying solver_settings_t, so any change to solver_settings_handle_t would have silently corrupted it.

All seven are now part of the public C API, implemented in cuopt_c.cpp beside the functions they belong with:

Parameters cuOptLoadParametersFromFile, cuOptDumpParametersToFile, cuOptGetNumSolverParameters, cuOptGetSolverParameterName
Solution stats cuOptSolutionIsMIP, cuOptGetLPSolverStats, cuOptGetMIPSolverStats

This covers the fields requested in #1202. Note for reviewers: #1202 proposed 11 individual getters and explicitly weighed that against a bulk call. This PR ships the bulk shape (two functions with nullable out-pointers), inherited from the original shim. If the individual-getter shape is preferred, say so and I will split them — flagging rather than silently closing #1202.

Build and CI

  • ./build.sh java and ./build.sh java --run-java-tests — an opt-in target, matching how cuvs exposes its Java build. Prefers cpp/build so it works without --install, and falls back to the conda prefix, which is what CI uses.
  • ci/build_java.sh calls build.sh rather than duplicating the invocation.
  • java/cuopt/scripts/test.sh carries a CUOPT_PRELOAD_LIBS escape hatch to work around [BUG] Build-tree libcuopt.so resolves librmm from the conda prefix instead of its own _deps, failing at dlopen #1704; it can be removed once that is fixed.
  • Java CI jobs moved off stale release/26.08 shared-workflow pins to @main with ci-conda:26.10-latest, and the redundant compute-matrix job was dropped.

Guard against JNI symbol drift

The bindings are hand-written, so a Java static native declaration and its Java_com_nvidia_..._name entry point can drift apart with nothing to catch it. JNI resolves methods lazily, so the library still loads and the failure only appears when something calls the method.

java/cuopt/scripts/check_jni_symbols.sh diffs the prototypes javac -h derives from the Java sources against the symbols the built library exports, and fails on a mismatch in either direction. It runs after every native build. It reads the built artifact rather than parsing source, so the macro-generated entry points need no special casing.

Packaging

Version is 26.10.0 — Maven has no notion of the zero-padded RAPIDS patch field, and 26.10.00 would sort oddly against a later 26.10.1. update-version.sh derives it. Sources and javadoc jars are attached, and the POM carries the url, licenses, scm, and developers metadata Maven Central requires.

Known gaps

These are deliberate, not oversights.

  1. The JNI still includes pdlp/cuopt_c_internal.hpp for the problem path — name setters, quadratic objective and quadratic constraint getters, and length reporting for duals and reduced costs. That couples libcuopt_jni.so to a specific libcuopt build rather than to a stable ABI, which matters before this is published as a binary artifact. Tracked in [FEA] Expand cuopt_c.h with problem-model accessors for non-Python language bindings #1703.
  2. Publishing is not wired up. Artifact metadata is complete; signing and upload are not. Tracked by the "Publishing & support" item in Java API support #1535.

Checklist

@nvidiacbrissette
nvidiacbrissette requested review from a team as code owners July 7, 2026 19:34
@nvidiacbrissette
nvidiacbrissette requested a review from tmckayus July 7, 2026 19:34
@copy-pr-bot

copy-pr-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Java bindings

Layer / File(s) Summary
Modeling contracts and expressions
java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/*
Adds Java variables, expressions, constraints, objectives, enums, callbacks, statistics, and validation behavior.
JNI bridge and native wrappers
java/cuopt/src/main/native/*, cpp/include/cuopt/mathematical_optimization/cuopt_c.h, cpp/src/pdlp/cuopt_c.cpp
Adds native declarations and JNI implementations for model creation, solving, settings, callbacks, persistence, solution fields, and statistics.
Problem modeling and solve flow
java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Problem.java
Adds model construction, MPS I/O, CSR/QCSR inspection, updates, relaxation, MIP starts, solving, and result propagation.
Build, packaging, and CI
java/cuopt/*, ci/*, build.sh, .github/workflows/*, dependencies.yaml
Adds Maven/CMake builds, native scripts, generated constants, Java dependencies, CI jobs, artifacts, and release version handling.
Validation and documentation
java/cuopt/src/test/*, docs/cuopt/source/cuopt-java/*, docs/cuopt/source/index.rst
Adds modeling and native integration tests plus Java quick-start, convex, and MIP documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: tmckayus, chris-maes, hlinsen

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: Java bindings for LP, MIP, and QP functionality.
Description check ✅ Passed The description directly explains the Java bindings, native C API changes, build and CI updates, packaging, tests, documentation, and known gaps.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@mlubin mlubin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw the PR is closed, sending my comments as I had them already written up.

Comment thread java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/BatchSolve.java Outdated

extern "C" {

cuopt_int_t cuOptLoadParametersFromFile(cuOptSolverSettings settings, const char* path);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should discuss merging these extensions into the C API.

@@ -0,0 +1,1367 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend avoiding a test dependency on the python interface. The java interface should stand on its own.

Comment thread java/cuopt/README.md Outdated
@@ -0,0 +1,25 @@
# cuOpt Java bindings (beta)

This directory is an isolated, customer-specific beta module for the cuOpt

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this how we want to ship it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would want to follow cuvs and try to publish to maven https://mvnrepository.com/artifact/com.nvidia.cuvs/cuvs-java

Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
@nvidiacbrissette

Copy link
Copy Markdown
Author

Sorry that was an accident. Reopening.

@@ -0,0 +1,28 @@
/home/cbrissette/cuopt/java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/BatchSolve.java

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these files ? may be we can delete all the run time files so developers can concentrate on main parts.

@ramakrishnap-nv ramakrishnap-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Focused review on APIs and shipping (vs how cuvs ships Java).

APIs: the surface is broad and, pleasingly, closely in sync with the Python API — the algebraic Problem layer matches Python's (camelCase) modeling methods almost 1:1, and DataModel maps cleanly (snake_case→camelCase). A few parity gaps and Java-idiom nits are noted inline.

Shipping: the main blockers — don't commit target/, and wire the build into CI/release the way cuvs does (ci/build_java.sh/ci/test_java.sh, dependencies.yaml java key, workflow jobs, version marker, docs toctree).

Non-blocking review comments below.

Comment thread java/cuopt/scripts/build_native.sh
Comment thread java/cuopt/pom.xml Outdated
Comment thread docs/cuopt/source/cuopt-java/index.rst
}

/** Return true for maximize and false for minimize, matching Python get_sense(). */
public boolean getSense() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity note (not a rename request): getSense() correctly matches the Python DataModel.get_sense() (bool, True=maximize) — good. Two parity gaps vs Python though: (1) Python puts set_initial_primal_solution/set_initial_dual_solution on DataModel, whereas here they're on SolverSettings; (2) Python DataModel also exposes getters this class seems to lack: get_quadratic_objective_{values,indices,offsets}, get_variable_names/get_row_names, get_objective_name/get_problem_name, get_ascii_row_types.

resetSolvedValues();
}

public Object getObjective() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getObjective() returns Object — callers must downcast. Prefer a typed return (or overloads). Same for SolverSettings.getTypedParameter() / getMipCallbacks(). (The modeling API otherwise tracks the Python Problem layer 1:1 — nice.)

Comment thread java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/CsrMatrix.java Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-examples.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-examples.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/index.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/index.rst Outdated
Comment thread java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/PDLPSolverMode.java Outdated
public enum ProblemCategory {
LP(0),
MIP(1),
IP(2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should deprecate IP across the whole code base.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed for Java.

Comment thread java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/SolverMethod.java Outdated

@chris-maes chris-maes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the JAVA API. Let's make sure that capitalization is consistent before merging.

``VariableType.SEMI_CONTINUOUS``; and
* ``ProblemCategory`` for the native problem classification.

``ProblemCategory.IP`` is deprecated. Java normalizes the legacy native IP

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to include or mention deprecated API. Remove all references to IP.

* ``ConstraintSense.LE``, ``ConstraintSense.GE``, and ``ConstraintSense.EQ``;
* ``VariableType.CONTINUOUS``, ``VariableType.INTEGER``, and
``VariableType.SEMI_CONTINUOUS``; and
* ``ProblemCategory`` for the native problem classification.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is ProblemCategory used for? Could we get away with just isMIP()?


``Constraint`` provides ``getSense``, ``getRHS``, ``getCoefficient``,
``getLinearExpression``, ``getQuadraticExpression``, ``isQuadratic``,
``computeSlack``, ``getSlack``, and ``getDualValue``.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is computeSlack needed. Let's provide the slack inside the C++ engine.


``Solution`` implements ``AutoCloseable`` and exposes:

* ``getPrimalSolution``, ``getDualSolution``, and ``getReducedCost``;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we want getPrimalSolution, getDualSolution and getReducedCost? These should be accessed via the constraints and variables.

* ``getSolveTime`` and ``getProblemCategory``; and
* ``getVars`` when variable names are available.

LP solutions additionally expose ``getLPStats``. ``LPStats`` contains primal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add attributes instead of stats.

MPS and Errors
--------------

``Problem.read`` and ``Problem.readMPS`` support MPS/QPS parsing, including a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove MPS read/write.

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

@chris-maes thanks for the review — replying to the C API comments in one place since they're connected.

Two facts first, because I think one of them changes the picture

There is no solution-scoped attribute API today. All five attribute getters are cuOptGetProblem*Attribute, and every CUOPT_ATTR_* / CUOPT_ARRAY_ATTR_* constant is problem-scoped. So these stats aren't reachable through attributes at present — your "and if not, they should be" is the actual proposal, and it means adding cuOptGetSolutionIntAttribute / cuOptGetSolutionFloatAttribute plus a CUOPT_SOLUTION_ATTR_* block.

None of these stats were reachable by any route before this PR. Checking main, of the eleven fields in #1202 — primal/dual residual, gap, iteration count, solved-by, presolve time, node count, simplex iterations, and the three violation metrics — zero are exposed. The solution accessors that do exist (cuOptGetTerminationStatus, cuOptGetObjectiveValue, cuOptGetMIPGap, cuOptGetSolutionBound, cuOptGetSolveTime, primal/dual/reduced costs) cover the outcome, not the diagnostics. Note cuOptGetMIPGap is the MIP optimality gap, which is a different quantity from the LP primal-dual gap in that list.

Why we think they're worth supporting

The data already exists on lp_solution_interface_t / mip_solution_interface_t, so this is pure exposure — no computation, no solve-time cost. Concretely:

  • Judging a non-optimal answer. When a solve returns on a time limit rather than OPTIMAL, residuals and gap are the only way to tell a nearly-converged solution from a useless one. Without them a C-ABI caller sees a status and a vector of numbers and cannot assess them.
  • Verifying a returned MIP solution. max_constraint_violation, max_int_violation, and max_variable_bound_violation are what a caller checks before acting on a solution in production. This is the one we'd argue hardest for.
  • Knowing which algorithm actually solved it. With CUOPT_METHOD_CONCURRENT the caller otherwise cannot tell whether PDLP or dual simplex produced the answer, which matters for tuning and reproducibility.
  • Performance work. Iteration counts, node counts, simplex iterations, and presolve time are the basis for benchmarking and spotting regressions.

Two more general points. Python already exposes all of this by binding to the C++ structs through Cython, so today the C ABI is a second-class path — that asymmetry is what #1202 was filed about. And users arriving from Gurobi or CPLEX expect IterCount / NodeCount / violation metrics as a matter of course.

Where we agree, and what we'd propose

Attributes are the right shape for these. All eleven are scalar ints and floats, which is exactly what the attribute model is for. There's also a maintenance argument specific to the bindings: cuOpt already generates CuOptConstants.java from constants.h, so a new attribute constant reaches Java with zero hand-written code, whereas a new function needs a JNI entry point, a native declaration, and a wrapper. Every future stat would be free on the binding side. That argues for attributes over the eleven individual getters proposed in #1202 as well.

What we'd suggest is a shape-based rule rather than attributes everywhere, since it's the rule the C API already follows:

  • scalars (stats, flags, counts) → attributes
  • homogeneous arrays sized by a known dimension (bounds, senses) → array attributes
  • structured, ragged, or multi-output → dedicated functions

The third case is why cuOptGetConstraintMatrix is a function rather than an attribute: CSR is three parallel arrays. The same applies to the quadratic constraint rows in #1703, where each row is a variable-length record containing variable-length arrays — no attribute scheme expresses that.

Happy to convert the two bulk cuOptGet{LP,MIP}SolverStats calls in this PR to solution attributes on that basis. It's more work than what's here now, but nothing has shipped yet, so it costs no deprecation or ABI break — this is the cheapest moment to change direction.

Conceded

cuOptSolutionIsMIP should go. You're right. It's redundant: Solution is already constructed with the problem's category, so the Java side has the answer without a native call, and a C caller normally holds the problem handle for cuOptIsMIP. We'll drop it.

The hyperparameters_only flag should go too — agreed that a public header shouldn't advertise the concept. We'll remove the flag and always dump non-hyperparameters.

One question back

On the parameter calls (cuOptLoadParametersFromFile, cuOptDumpParametersToFile, cuOptGetNumSolverParameters, cuOptGetSolverParameterName): the use case is settings introspection and file round-tripping, matching what Python offers — Java's getSolverSettingNames() and load/dumpSettingsToFile are built on them. They're convenience rather than necessity, and we're happy to drop them from this PR if you'd rather keep the C API lean and revisit separately.

Reworks the C API surface this PR adds, following review. Seven functions
become two.

Statistics are now read with cuOptGetSolutionIntAttribute and
cuOptGetSolutionFloatAttribute using CUOPT_SOLUTION_ATTR_* selectors, in place
of cuOptGetLPSolverStats and cuOptGetMIPSolverStats. This matches how problem
data is already read, and it means a future statistic is a new constant rather
than a new exported symbol, so existing callers keep working without a relink.
It also costs the bindings nothing: CuOptConstants.java is generated from
constants.h, so a new selector reaches Java with no hand-written code, where a
new function needs a JNI entry point, a native declaration, and a wrapper.

Selectors are numbered in their own range so a problem selector passed to a
solution accessor is rejected rather than silently read, and LP selectors
require an LP solution while MIP selectors require a MIP solution.

cuOptSolutionIsMIP is removed as redundant. Solution is constructed with the
category of the problem it was solved for, so it already knows whether it came
from the MIP solver, and a C caller holding the problem handle has cuOptIsMIP.

The four parameter-file and parameter-enumeration entry points are removed
rather than promoted. Nothing needs them to build, solve, or read a solution,
and one of them exposed the hyperparameters concept through a public header,
which it should not. The capability is recorded in NVIDIA#1705 so it is deferred
rather than lost. On the Java side this drops getSolverSettingNames, toDict,
and the settings-file methods; setOptimalityTolerance now names the six LP
tolerances explicitly instead of discovering them by matching parameter names,
which is also less fragile than the previous filter.

Verified: 69/69 C API gtests, including new tests that the accessors reject a
mismatched value type, the wrong solver's selectors, unknown selectors, and
null arguments; 26/26 Java tests; all pre-commit hooks.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
clang-format widens the value column to the longest macro name in the block.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 5972293

@mlubin

mlubin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

IMO changes to the C API should be in a separate PR, not combined with a 7000 line diff here.

NVIDIA#1705 tracked the removed parameter enumeration and settings-file entry points
as deferred work. mlubin pointed out that bindings can be generated from
constants.h, which makes runtime enumeration unnecessary.

That holds structurally, not just in practice: every one of the 117 parameter
registrations in solver_settings.cu is written as a CUOPT_* macro from
constants.h, and none of the 109 distinct registered names is absent from the
header. The list get_parameter_names() returns is therefore derived from the
same header the bindings generate from, and cannot contain anything extra.
Loading and dumping a settings file is expressible on top of cuOptSetParameter
and cuOptGetParameter, so that half needs no new C surface either.

NVIDIA#1705 is closed. Point the README at the reasoning rather than describing the
capability as pending.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 4eb70d8

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

@mlubin agreed — the C API changes are now #1715, off main, 318 lines across 4 files with no reference to Java.

This PR is stacked on it. The cpp/ changes still show in this diff because the Java bindings call cuOptGetSolutionIntAttribute / cuOptGetSolutionFloatAttribute and would not build without them; they will drop out of this diff automatically once #1715 merges and main is merged back in.

Two related issues were also split out of the earlier discussion, so each is reviewable on its own:

#1705 is closed: you were right that runtime parameter enumeration is unnecessary. Every one of the 117 parameter registrations is written as a CUOPT_* macro from constants.h, and none of the 109 distinct registered names is missing from the header, so a binding generated from that header has the complete set by construction.

Documentation wording and the scoped solution guard in the C API tests, kept
identical to the branch behind NVIDIA#1715 so this stack stays consistent until that
PR merges and these files drop out of this diff.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test d981aec

Keeps the cpp/ files in this stack identical to the branch behind NVIDIA#1715.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 6a04604

rapids-bot Bot pushed a commit that referenced this pull request Aug 14, 2026
Adds two C API accessors so solver statistics are reachable from the C ABI, and closes #1202.

Split out of #1524 at @mlubin's request, so the C API can be reviewed without the Java diff attached. The Java bindings are the first consumer but nothing here refers to them.

### The gap

The C API reports the *outcome* of a solve — `cuOptGetTerminationStatus`, `cuOptGetObjectiveValue`, `cuOptGetMIPGap`, `cuOptGetSolutionBound`, `cuOptGetSolveTime`, and the primal/dual/reduced-cost arrays — but none of the diagnostics the C++ solution interfaces already carry. Of the eleven fields listed in #1202, **zero** are reachable today.

Python sidesteps this by binding to the C++ structs through Cython, so the C ABI is a second-class path and every non-Python binding hits the same wall.

Concretely, what these unlock:

- **Judging a non-optimal answer.** When a solve stops on a time limit, residuals and gap are the only way to distinguish a nearly-converged solution from a useless one.
- **Verifying a returned MIP solution.** The three violation magnitudes are what a caller checks before acting on a solution.
- **Knowing which algorithm solved it.** Under `CUOPT_METHOD_CONCURRENT` the caller otherwise cannot tell PDLP from dual simplex.
- **Performance work.** Iteration counts, nodes, simplex iterations, and presolve time.

### Why attributes rather than one getter per statistic

#1202 proposed eleven individual getters. This uses the attribute model instead, following review feedback from @chris-maes:

- it matches how problem data is already read (`cuOptGetProblemIntAttribute` and friends);
- a future statistic is a new constant rather than a new exported symbol, so existing callers need no relink;
- bindings that generate from `constants.h` — as the Java bindings already do for `CuOptConstants.java` — pick up new statistics with no hand-written code at all.

Structured data stays on dedicated functions, which is the existing convention: `cuOptGetConstraintMatrix` is a function because CSR is three parallel arrays, and the same will apply to the quadratic constraint rows in #1703. The rule is scalars and homogeneous arrays as attributes, ragged or multi-output as functions.

### Safety

Solution selectors are numbered in their own range (300+), so a problem selector passed to a solution accessor, or the reverse, is rejected rather than silently read. LP selectors require an LP solution and MIP selectors require a MIP solution, since the two come from different solvers; `CUOPT_ATTR_IS_MIP` on the originating problem says which set applies.

The values are read straight off `lp_solution_interface_t` / `mip_solution_interface_t`, so this is exposure only — no computation, no solve-time cost.

### Tests

`c_api.lp_solution_attributes` and `c_api.mip_solution_attributes` cover both solvers and the ways a caller can get it wrong: a float selector through the integer accessor and the reverse, the other solver's selectors, unknown selectors, and null arguments.

Float outputs are seeded with `NaN` rather than a numeric sentinel, since the solver cannot legitimately produce `NaN` — so an accessor that never writes its output is caught, where a numeric sentinel would be indistinguishable from a real result.

Verified locally: **69/69** `C_API_TEST` cases pass with the full LP/MIP/QP dataset.

Authors:
  - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv)

Approvers:
  - Miles Lubin (https://github.com/mlubin)

URL: #1715
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 8d854d3

Three of chris-maes's comments that stand on their own, ahead of the ones that
depend on how far the API moves toward an attribute model.

Remove IP from ProblemCategory. The engine still reports a third category for a
problem whose variables are all discrete, so the mapping cannot simply be
deleted: problem_category_t::IP is returned from cpu_optimization_problem.cpp
when every variable is discrete, and fromNative would throw on it. The constant
is gone from the public enum and the native value is folded into MIP instead,
with a comment recording why the mapping exists.

Consolidate file IO on read and write. readMPS forced the MPS parser where read
already dispatches on the file extension, and Python has deprecated its
equivalent, so the Java API should not be introducing the name. writeMPS is now
write. This also removes the parseMPSProblem JNI entry point, which was the
only caller of io::read_mps, so the bindings no longer reach into the parser
for a second, redundant path.

Rename getCSR and getQCSR. The review asked what a "linear objective matrix"
was, and the answer is that the documentation was wrong: getCSR returns the
constraint matrix, not an objective. They are now getConstraintMatrix and
getQuadraticObjectiveMatrix, matching cuOptGetConstraintMatrix in the C API,
and both carry a line saying what they return.

Verified: 26/26 Java tests, 59 JNI symbols matched, all pre-commit hooks.
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test dc8b4b0

The public enum constant is gone, so the paragraph documented an API that no
longer exists. The only remaining mention is a comment in ProblemCategory
explaining why the native value is folded into MIP, which has to stay while the
engine still reports it.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

@chris-maes before I work through the rest of your review, I'd like to check the direction, because I think the remaining comments share one underlying question and answering it settles most of them at once.

What's already done

  • IP is gone from ProblemCategory and from the docs. The native value still has to be mapped, since the engine returns a third category when every variable is discrete (cpu_optimization_problem.cpp), so it is folded into MIP with a comment saying why.
  • readMPS / writeMPS are gone; read dispatches on the file extension and write replaces writeMPS. This also deleted the parseMPSProblem JNI entry point, which was the only remaining caller of io::read_mps.
  • getCSR / getQCSR are now getConstraintMatrix / getQuadraticObjectiveMatrix. You were right to ask what a "linear objective matrix" was — the documentation was wrong, getCSR returned the constraint matrix.
  • The five C API comments are addressed and merged separately as Expose solver statistics as scalar solution attributes #1715.

On "fluent": @mlubin noted it is standard terminology for this style of API, so I have left the wording unless you would still prefer it changed.

The question

The Java surface was built for parity with the Python API — that was its design goal, and it is why the shape looks the way it does. Your review is asking it to be consistent with the C API instead, which took the attribute direction in #1549.

Both are reasonable, but they point different ways, and five of the six items you have asked to remove exist in the Python API today:

Java Python equivalent
getProblemCategory get_problem_category — present
getPrimalSolution get_primal_solution — present
getDualSolution get_dual_solution — present
getReducedCost get_reduced_cost — present
computeSlack compute_slack — present
getObjective get_objective — present
isSolved absent

So the choice is roughly:

(a) Java aligns with the C API and diverges from Python. Java becomes the model the Python API moves toward later. Users moving between the two see different shapes until Python follows.

(b) Java matches Python for now, and both move to an attribute model together as a separate piece of work.

isSolved is the exception either way — it is not in Python and getStatus covers it, so I will remove it regardless.

What each remaining item would mean

ObjectiveExpression — it is the interface returned by getObjective, implemented only by LinearExpression and QuadraticExpression. Removing it means getObjective returns one of those two, so callers either get overloads or an instanceof check. Cheap either way; mostly a question of what you want the return type to be.

ProblemCategory — one wrinkle worth flagging. You asked why cuOptSolutionIsMIP existed when cuOptIsMIP was available. I removed it, and Solution now derives isMIP() from the problem's ProblemCategory rather than calling native. If ProblemCategory also goes, Solution needs CUOPT_ATTR_IS_MIP as its source instead. Fine either way, but the two changes interact.

getPrimalSolution / getDualSolution / getReducedCost — these already exist per-variable and per-constraint (Variable.getValue(), Variable.getReducedCost(), Constraint.getDualValue()), populated after each solve. The array forms are an additional bulk path. Removing them is straightforward; the thing lost is copying a whole solution vector out in one call, which matters for large models. Happy to remove if you would rather have one way to do it.

computeSlack in the C++ engine — agreed on the principle: if each binding computes slack itself, they can disagree on tolerances or on how quadratic constraints are handled. That is a C++ change though, so by the precedent from #1715 it should be its own PR. Can that follow separately rather than block this one?

"Attributes instead of stats" — this is the one I would most like pinned down, since it subsumes several of the above. Do you mean:

  1. a thin Java wrapper over the CUOPT_SOLUTION_ATTR_* accessors that just merged, keeping LPStats / MIPStats as the presentation; or
  2. a genuinely attribute-shaped Java API, where a caller fetches values by selector rather than through typed accessors?

Option 2 is a much larger change and would be unusual for Java, where typed accessors are the norm and an untyped selector API gives up compile-time checking. Option 1 I can do now.

Last thing

This PR is around 6,700 lines and has been open since July. Which of these do you consider blocking for merge, and which could land as follow-ups against the Java module once it is in? If most can follow, the bindings become reviewable on their own merits and the API refinement can proceed without holding up CI coverage and the build integration.

getStatus already answers the question: it stays NO_TERMINATION until a solve
populates it, so isSolved was a second way to ask something the API already
told you. This is the one item from the review that needs no decision on the
wider API direction, since the Python API has no equivalent either.

The private solved field went with it, as isSolved was its only reader.

Verified: 26/26 Java tests.
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 8b178d9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants